import os
import shutil
from datetime import datetime
from pathlib import Path, PurePath

from flask import (
    Flask, render_template, redirect, url_for, g, request, session, send_file
)
from loguru import logger

from .utils import *


# from flask import (
#     Blueprint, flash, g, redirect, render_template, request, session, url_for
# )

current_folder = os.path.abspath(os.path.dirname(__file__))
template_folder = os.path.join(current_folder, 'templates')
static_folder = os.path.join(current_folder, 'static')


def create_app(config):
    app = Flask('TinyFileManager-Py',
                template_folder=template_folder,
                static_folder=static_folder,)
    app.config.update(SECRET_KEY='tinyfilemanagerpysessionkey')

    @app.before_request
    def add_config_to_g():
        g.config = config
        pass

    @app.after_request
    def add_header(r):
        """
        Add headers to both force latest IE rendering engine or Chrome Frame,
        and also to cache the rendered page for 10 minutes.
        """
        r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
        r.headers["Pragma"] = "no-cache"
        r.headers["Expires"] = "0"
        r.headers['Cache-Control'] = 'public, max-age=0'
        return r

    def validate_path(path, need_exists=True):
        path = remove_starting_slash(path)
        try:
            root_path = Path(g.config['Directory']).resolve()
            cur_path = Path(root_path, path)
            PurePath(cur_path).relative_to(PurePath(root_path))
        except Exception as e:
            logger.exception(e)
            show_message(f'Invalid path - {path}', 'error')
            return False, redirect(url_for('main', path=''))

        if need_exists and not cur_path.exists():
            logger.exception(f'{cur_path} not exists')
            show_message(f'Nonexist path - {path}', 'error')
            return False, redirect(url_for('main', path=''))
        return True, cur_path

    def add_message_to_template(template_data):
        if 'message' in session:
            template_data['message'] = session['message']
            del session['message']

    def show_message(text, status='ok'):
        session['message'] = {'text': text, 'status': status, }

    @app.route('/p/', defaults={'path': ''})
    @app.route('/p/<path:path>')
    def main(path):
        """
        Display Folder/File View
        """

        ok, res = validate_path(path)
        if not ok:
            return res
        cur_path = res

        if cur_path.is_dir():
            return show_folder(cur_path)
        elif cur_path.is_file():
            return download_file(cur_path)

    def show_folder(path):
        """
        Show folder details
        """
        template_data = {

        }
        add_message_to_template(template_data)

        root_path = Path(g.config['Directory']).resolve()
        relative_path = get_relative_path(root_path, path)

        template_data['folder_path'] = str(relative_path)

        path_list = []
        temp_path = PurePath()
        for p in relative_path.parts:
            temp_path = PurePath(temp_path, p)
            path_list.append({'name': p, 'path': str(temp_path)})
        template_data['path_list'] = path_list

        folders = []
        files = []
        ii = 3399
        ik = 6070
        total_size = 0
        try:
            for child in path.iterdir():
                child_name = child.parts[-1]
                relative_path = get_relative_path(root_path, child)
                if not child.is_symlink():
                    stat = child.stat()
                else:
                    stat = child.lstat()

                try:
                    owner = f'{child.owner()}:{child.group()}'
                except:
                    owner = 'NA'

                if not g.config['ShowHidden'] and child_name.startswith('.'):
                    continue

                if child.is_dir():
                    size = stat.st_size
                    if g.config['CalculateFolderSize']:
                        size = get_folder_size(child)
                    total_size += size

                    folders.append({
                        'name': child_name,
                        'path': str(relative_path),
                        'is_symlink': child.is_symlink(),
                        'symlink_to': os.path.realpath(child),
                        'modified_time': stat.st_mtime,
                        'modified_time_str': convert_time_to_str(stat.st_mtime),
                        'size': size,
                        'size_str': convert_size_to_str(size),
                        'owner': owner,
                        'permission': convert_mode_to_str(stat.st_mode),
                        'ii': ii,
                    })
                    ii += 1
                    pass
                elif child.is_file():
                    total_size += stat.st_size
                    files.append({
                        'name': child_name,
                        'path': str(relative_path),
                        'icon_class': get_file_icon_class(child_name),
                        'is_symlink': child.is_symlink(),
                        'symlink_to': os.path.realpath(child),
                        'modified_time': stat.st_mtime,
                        'modified_time_str': convert_time_to_str(stat.st_mtime),
                        'size': stat.st_size,
                        'size_str': convert_size_to_str(stat.st_size),
                        'owner': owner,
                        'permission': convert_mode_to_str(stat.st_mode),
                        'ik': ik,
                    })
                    ik += 1
                    pass
                else:
                    # special file ?
                    pass
        except:
            show_message('Failed to show folder', 'error')
            add_message_to_template(template_data)

        try:
            parent_folder_path = path.parent
            rel_path = get_relative_path(root_path, parent_folder_path)
            template_data['parent_folder'] = str(rel_path)
        except:
            pass

        template_data['folders'] = folders
        template_data['files'] = files

        template_data['file_num'] = len(files)
        template_data['folder_num'] = len(folders)
        template_data['total_size'] = convert_size_to_str(total_size)

        disk_stat = shutil.disk_usage(path)
        template_data['disk_free_size'] = convert_size_to_str(disk_stat.free)
        template_data['disk_used_size'] = convert_size_to_str(disk_stat.used)
        template_data['disk_total_size'] = convert_size_to_str(disk_stat.total)

        return render_template('main.html', **template_data)

    def download_file(path):
        return send_file(path, conditional=True, as_attachment=True)

    @app.route('/')
    def redirect_to_path():
        return redirect(url_for('main', path=''))

    @app.route('/upload/', defaults={'path': ''})
    @app.route('/upload/<path:path>')
    def upload(path):
        """
        Display Upload View
        """
        template_data = {

        }
        add_message_to_template(template_data)

        ok, res = validate_path(path)
        if not ok:
            return res
        cur_path = res
        relative_path = get_relative_path(g.config['Directory'], cur_path)

        if not cur_path.is_dir():
            return redirect(url_for('upload', path=redirect_path.parent))

        if str(relative_path) == '.':
            relative_path = ''

        template_data['des_folder'] = relative_path

        return render_template('upload.html', **template_data)

    @app.route('/upload_handler', methods=['POST'])
    def upload_handler():
        """
        Handle upload operation
        """

        respond = {
            'status': 'error',
            'info': 'Oops! Try again',
        }

        chunk_index = int(request.form['dzchunkindex'])
        chunk_total = int(request.form['dztotalchunkcount'])
        to_folder = request.form['to']
        to_filename = request.form['fullpath']

        to_folder_path = Path(g.config['Directory'], to_folder)
        ok, res = validate_path(to_folder_path)
        if not ok:
            respond['info'] = 'Invalid destination folder'
            return respond
        cur_path = res

        if not cur_path.is_dir():
            respond['info'] = 'Invalid destination folder'
            return respond

        to_file_path = Path(cur_path, to_filename)
        if to_file_path.exists():
            respond['info'] = 'File with same name exists'
            return respond

        temp_file = Path(cur_path, f'{to_filename}.part')

        # handle file pieces
        try:
            with open(temp_file, 'ab') as write_fp:
                write_fp.seek(0, 2)
                read_fp = request.files['file']
                while True:
                    buf = read_fp.read(1024*4)
                    if len(buf) <= 0:
                        break
                    write_fp.write(buf)
            chunk_index += 1
        except Exception as e:
            logger.exception(e)
            respond['info'] = 'Failed to write file'
            return respond

        if chunk_index == chunk_total:
            # done uploading
            try:
                temp_file.rename(to_filename)
            except Exception as e:
                logger.exception(e)
                respond['info'] = 'Cannot upload requested file'
                return respond
            pass
            respond = {
                'info': f'Successful upload file - {to_filename}',
                'status': 'success',
            }
        else:
            respond = {
                'info': f'Successful upload file - {to_filename}',
                'status': 'success',
            }
        return respond

    @app.route('/delete/<path:path>')
    def delete(path):
        """
        Handle delete operation
        """

        ok, res = validate_path(path)
        if not ok:
            return res
        cur_path = res
        relative_path = get_relative_path(g.config['Directory'], cur_path)

        if str(redirect_to_path) == '.':
            show_message(f'Cannot delete root directory', 'error')
            return redirect(url_for('main', path=''))

        if cur_path.is_file():
            try:
                os.remove(path)
            except OSError:
                show_message(
                    f'Failed to delete file - {relative_path}', 'error')
                return redirect(url_for('main', path=relative_path.parent))

            show_message(f'Deleted file - {relative_path}')
            return redirect(url_for('main', path=relative_path.parent))
        elif cur_path.is_dir():
            try:
                shutil.rmtree(path)
            except OSError:
                show_message(
                    f'Failed to delete folder - {relative_path}', 'error')
                return redirect(url_for('main', path=relative_path.parent))

            show_message(f'Deleted folder - {relative_path}')
            return redirect(url_for('main', path=relative_path.parent))
            pass

        logger.exception(f'cannot handle path - {path}')
        return redirect(url_for('main', path=''))

    @app.route('/rename/<path:path>')
    def rename(path):
        """
        Handle rename operation
        """

        ok, res = validate_path(path)
        if not ok:
            return res
        cur_path = res
        relative_path = get_relative_path(g.config['Directory'], cur_path)

        if str(redirect_to_path) == '.':
            show_message(f'Cannot delete root directory', 'error')
            return redirect(url_for('main', path=''))

        new_name = request.args.get('to')
        if new_name is None or len(new_name) == 0:
            show_message(f'Invalid rename data', 'error')
            return redirect(url_for('main', path=relative_path.parent))

        if cur_path.with_name(new_name).exists():
            show_message(f'File with same name exists', 'error')
            return redirect(url_for('main', path=relative_path.parent))

        try:
            cur_path.rename(new_name)
        except OSError:
            show_message(
                f'Failed to rename - {cur_path.parts[-1]} to {new_name}', 'error')
            return redirect(url_for('main', path=relative_path.parent))

        show_message(f'Rename done - {cur_path.parts[-1]} to {new_name}')
        return redirect(url_for('main', path=relative_path.parent))

    @app.route('/copymove/<path:path>')
    def copymove(path):
        """
        Handle copy and move opeartion
        """

        ok, res = validate_path(path)
        if not ok:
            return redirect(url_for('main', path=''))
        path = res
        relative_path = get_relative_path(g.config['Directory'], path)

        if not 'to' in request.args:
            return redirect(url_for('copymove', path=relative_path, to=''))
        to_path = request.args['to']
        to_path = remove_starting_slash(to_path)
        ok, res = validate_path(to_path)
        to_abs_path = res
        if not ok or not to_abs_path.is_dir():
            show_message(f'Invalid destination path - {to_path}', 'error')
            return redirect(url_for('copymove', path=relative_path, to=''))

        root_path = Path(g.config['Directory']).resolve()
        relative_path = get_relative_path(root_path, path)
        to_path_relative = get_relative_path(root_path, to_abs_path)
        to_path_relative_str = str(to_path_relative)
        to_path_relative_str = '/' + \
            to_path_relative_str if to_path_relative_str != '.' else '/'

        if 'action' in request.args:
            if request.args['action'] not in ('move', 'copy'):
                show_message(f'Invalid action', 'error')
                return redirect(url_for('copymove', path=relative_path, to=to_path_relative))
            return do_copymove(path, to_abs_path, request.args['action'])

        template_data = {

        }
        add_message_to_template(template_data)

        template_data['from_path'] = '/' + str(relative_path)
        template_data['to_path'] = to_path_relative_str
        template_data['from_folder'] = relative_path.parent

        if str(to_path_relative) != '.':
            parent_folder = str(to_path_relative.parent)
            parent_folder = '/' + parent_folder
            template_data['parent_path'] = parent_folder

        folders = []
        for child in to_abs_path.iterdir():
            if not child.is_dir():
                continue
            child_name = child.parts[-1]
            relative_path = get_relative_path(root_path, child)
            folders.append({'name': child_name, 'path': str(relative_path)})

        template_data['folders'] = folders

        return render_template('copymove.html', **template_data)

    def do_copymove(from_path, to_path, action):
        filename = from_path.parts[-1]
        root_path = Path(g.config['Directory']).resolve()
        from_path_relative = get_relative_path(root_path, from_path)
        to_path_relative = get_relative_path(root_path, to_path)

        new_path = Path(to_path, filename)

        if new_path.exists() and (action == 'copy' or (action == 'move' and not new_path.samefile(from_path))):
            if g.config['CopyConflictRename']:
                i = 1
                while True:
                    new_filename = f'{filename}({i})'
                    if not Path(to_path, new_filename).exists():
                        filename = new_filename
                        break
                    else:
                        i += 1
            else:
                show_message(f'Target path exists', 'error')
                return redirect(url_for('copymove', path=from_path_relative, to=to_path_relative))

        try:
            if action == 'copy':
                do_copy(from_path, to_path, filename)
            else:
                do_move(from_path, to_path, filename)
        except Exception as e:
            logger.exception(e)
            show_message(f'Failed to copy or move files/folders', 'error')
            return redirect(url_for('main', path=from_path_relative.parent))

        show_message(f'Done copy or move files/folders')
        return redirect(url_for('main', path=from_path_relative.parent))

    @app.route('/newfolder/<path:path>')
    def new_folder(path):
        """
        Handle new folder operation
        """
        ok, res = validate_path(path, need_exists=False)
        if not ok:
            return res
        cur_path = res

        if cur_path.exists():
            show_message(f'Target path exists - {path}', 'error')
            return redirect(url_for('main', path=''))

        try:
            cur_path.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            logger.exception(e)
            show_message(f'Failed to create new folder - {path}', 'error')
            return redirect(url_for('main', path=''))

        show_message(f'Created new folder - {path}')
        return redirect(url_for('main', path=path))

    @app.route('/batch_op', methods=['POST'])
    def batch_operation():
        """
        Handle batch operation
        """
        form = request.form
        if not ('p' in form and 'group' in form and 'file[]' in form):
            show_message(f'Invalid batch operation request', 'error')
            if 'p' in form:
                return redirect(url_for('main', path=form['p']))
            return redirect(url_for('main', path=''))

        if len(form.getlist('file[]')) <= 0:
            show_message(f'Invalid batch operation request', 'error')
            return redirect(url_for('main', path=form['p']))

        if 'delete' in form:
            return do_batch_delete()
        elif 'copy' in form:
            return go_batch_copymove()

        show_message(f'Invalid batch operation request', 'error')
        if 'p' in form:
            return redirect(url_for('main', path=form['p']))
        return redirect(url_for('main', path=''))

    def do_batch_delete():
        form = request.form
        request_path = form['p']

        ok, res = validate_path(request_path)
        if not ok:
            return redirect(url_for('main', path=''))
        path = res
        if not path.is_dir():
            show_message(f'Invalid path - {path}', 'error')
            return redirect(url_for('main', path=''))

        try:
            for file in form.getlist('file[]'):
                file_path = path.joinpath(file)
                if not file_path.exists():
                    continue
                if file_path.is_file():
                    os.remove(file_path)
                else:
                    shutil.rmtree(file_path)
        except Exception as e:
            logger.exception(e)
            show_message(f'Error while deleting items', 'error')
            return redirect(url_for('main', path=request_path))

        show_message(f'Selected files and folder deleted')
        return redirect(url_for('main', path=request_path))

    def go_batch_copymove():
        form = request.form
        request_path = form['p']

        ok, res = validate_path(request_path)
        if not ok:
            return redirect(url_for('main', path=''))
        path = res
        if not path.is_dir():
            show_message(f'Invalid path - {path}', 'error')
            return redirect(url_for('main', path=''))

        files = form.getlist('file[]')
        session['batch_copymove'] = {
            'p': form['p'],
            'files': files
        }
        return redirect(url_for('batch_copymove_view'))

    @app.route('/batch_copymove')
    def batch_copymove_view():
        if 'batch_copymove' not in session:
            return redirect(url_for('main', path=''))

        path = session['batch_copymove']['p']
        ok, res = validate_path(path)
        if not ok:
            return redirect(url_for('main', path=''))
        path = res
        relative_path = get_relative_path(g.config['Directory'], path)

        if not 'to' in request.args:
            return redirect(url_for('batch_copymove_view', to=''))
        to_path = request.args['to']
        to_path = remove_starting_slash(to_path)
        ok, res = validate_path(to_path)
        to_abs_path = res
        if not ok or not to_abs_path.is_dir():
            show_message(f'Invalid destination path - {to_path}', 'error')
            return redirect(url_for('batch_copymove_view', to=''))

        root_path = Path(g.config['Directory']).resolve()
        relative_path = get_relative_path(root_path, path)
        to_path_relative = get_relative_path(root_path, to_abs_path)
        to_path_relative_str = str(to_path_relative)
        to_path_relative_str = '/' + \
            to_path_relative_str if to_path_relative_str != '.' else '/'

        files = []
        folders = []
        for name in session['batch_copymove']['files']:
            if Path(path, name).is_file():
                files.append(name)
            else:
                folders.append(name)

        if 'action' in request.args:
            if request.args['action'] not in ('move', 'copy'):
                show_message(f'Invalid action', 'error')
                return redirect(url_for('batch_copymove',  to=to_path_relative))
            return do_batch_copymove(path, to_abs_path, request.args['action'])

        template_data = {

        }
        add_message_to_template(template_data)

        template_data['from_path'] = '/' + str(relative_path)
        template_data['to_path'] = to_path_relative_str
        template_data['from_folder'] = relative_path.parent
        template_data['target_files'] = files
        template_data['target_folders'] = folders

        if str(to_path_relative) != '.':
            parent_folder = str(to_path_relative.parent)
            parent_folder = '/' + parent_folder
            template_data['parent_path'] = parent_folder

        folders = []
        for child in to_abs_path.iterdir():
            if not child.is_dir():
                continue
            child_name = child.parts[-1]
            relative_path = get_relative_path(root_path, child)
            folders.append({'name': child_name, 'path': str(relative_path)})

        template_data['folders'] = folders

        return render_template('batch_copymove.html', **template_data)

    def do_batch_copymove(from_path, to_path, action):
        root_path = Path(g.config['Directory']).resolve()
        from_path_relative = get_relative_path(root_path, from_path)
        files = session['batch_copymove']['files']
        try:
            for file in files:
                batch_copymove_do_one_item(
                    from_path.joinpath(file), to_path, action)
        except Exception as e:
            logger.exception(e)
            show_message(f'Failed to copy or move files/folders', 'error')
            return redirect(url_for('main', path=from_path_relative.parent))

        show_message(f'Done copy or move files/folders')
        return redirect(url_for('main', path=from_path_relative.parent))

    def batch_copymove_do_one_item(from_path, to_path, action):
        filename = from_path.parts[-1]
        root_path = Path(g.config['Directory']).resolve()
        from_path_relative = get_relative_path(root_path, from_path)
        to_path_relative = get_relative_path(root_path, to_path)

        new_path = Path(to_path, filename)

        if new_path.exists() and (action == 'copy' or (action == 'move' and not new_path.samefile(from_path))):
            if g.config['CopyConflictRename']:
                i = 1
                while True:
                    new_filename = f'{filename}({i})'
                    if not Path(to_path, new_filename).exists():
                        filename = new_filename
                        break
                    else:
                        i += 1
            else:
                raise Exception

        if action == 'copy':
            do_copy(from_path, to_path, filename)
        else:
            do_move(from_path, to_path, filename)
        pass

    return app


class TinyFileManagerApp:

    def __init__(self, config):
        self.config = config
        self.app = create_app(config)

    def run(self):
        self.app.run(host='0.0.0.0', port=8000, debug=True)

    pass
